Linguaggio C

 

per collaborazioni, commenti, critiche, e altro contattateci alla e-mail: clubinfo@libero.it risponderemo al più presto!

Strutture di controllo

di Luca Sabatucci

Lezione 5

Pagina principale | Lezione precedente | Lezione successiva


Sequenza

Le strutture di controllo fanno riferimento ai costrutti del linguaggio usati per indicare l'ordine in cui le singole istruzioni devono essere eseguite. Le strutture di controllo di un linguaggio hanno un enorme impatto sul alcune delle caratteristiche del linguaggio stesso. In particolare sulla leggibilità e sulla modificabilità. La struttura di controllo per il flusso più semplice è la sequenza.

La sequenza è il meccanismo usato per indicare l'ordine di esecuzione delle istruzioni.


istruzione1;
istruzione2;

In C ogni istruzione è terminata dal punto e virgola. Gli spazi e il carattere di fine riga, non sono significativi. Lo stesso codice si otterrebbe con un unica riga:


istruzione1; istruzione2;

Il meccanismo di sequenza indica che l'istruzione1 sarà eseguita prima dell'istruzione2.

Una singola istruzione può essere sempre sostituita da un blocco di istruzioni.
Un blocco è costituito da un insieme di istruzioni comprese tra le parentesi graffe. E' possibile porre un blocco in un punto qualunque del listato, dove è possibile porre una istruzione.
I blocchi hanno due utilità:

Mentre il primo caso è molto usato e lo vedremo spessissimo, il secondo è abbastanza insolito. Vediamo un esempio del primo caso: supponiamo di misurare la temperatura di un container; se questa supera i livelli di guardia dobbiamo attivare il sistema di controllo e avvisare l'addetto alla sicurezza.


if ( misura_temperatura(Container) ) {
	attiva_controllo();
	avvisa_sicurezza();
}

Vediamo adesso un esempio (insolito) di blocco all'interno di un blocco:


int misura_temperatura(locazione target) {
	int temperatura;

	{
		int contatore;
		...
	}

	...
}	

Istruzioni di selezione

Sono due le istruzioni per la selezione. Una di queste l'abbiamo già vista in parecchi esempi è if, mentre l'altra è switch

Vediamole una per una.

Istruzione if

L'istruzione if permette di effettuare delle scelte. La forma generale dell'istruzione if è la seguente:


if (espressione booleana) istruzione;

Il significato è semplice: se il valore dell'espressione booleana è vero, allora l'istruzione viene eseguita, altrimenti di passa all'istruzione successiva. Si noti (riprendendo l'esempio precedente del container) che questa forma è sbagliata:


if ( misura_temperatura(Container) )
	attiva_controllo();
	avvisa_sicurezza();

Infatti senza l'uso delle parentesi graffe solo la prima istruzione attiva_controllo() è condizionata dalla temperatura. La prima istruzione invece viene eseguita comunque.

Si noti inoltre come grazie all'indentazione il listato sia più leggibile. L'indentazione è l'uso corretto di spazi e caratteri di tabulazione per creare delle rientranze. Tali rientranze indicano che le istruzioni sono istruzioni condizionali. Le regole dell'indentazione dicono che ogni blocco logico deve essere indentato rispetto al precedente.


if ( misura_temperatura(Container) ) {
		
	if ( attiva_controllo() = false ) {		/* inizio del blocco di livello 1 */
		attiva_allarme_generale();			/* inizio del blocco di livello 2 */
		attiva_sistema_raffreddamento();
	}										/* fine del blocco di livello 2 */

	avvisa_sicurezza();		
}											/* fine del blocco di livello 1 */

In questa maniera si individuano subito le dipendenze ed è più facile seguire ad occhio il flusso del programma.

Questo esempio è servito anche a mostrare come l'istruzione if sia stata usata all'interno di un blocco if.

Facciamo un esempio concreto di una funzione usata per determinare il massimo tra due numeri:


int massimo(int a, int b) {
	int max = b;	

	if (a > b)
		max = a;
		
	return max;
}			

Abbiamo creato una funzione che dati due interi (a e b) restituisce un intero. Dentro la funzione si dichiara una variabile locale chiamata max destinata a contenere il valore da restituire. La variabile viene inizializzata al valore di b, una delle due variabili in ingresso.
Quindi con una istruzione di controllo si effettua il confronto tra le due variabili inserite, a e b e se l'espressione da valore vero (cioè se a è maggiore di b) allora si effettua l'assegnamento max = a; cioè il valore da restituire viene cambiato. In questo modo se l'espressione è vera viene restituito a, se l'espressione è falsa viene restituito b. In ogni caso viene restituito il più grande.

Costrutto if..else

Esistono dei casi in cui data una condizione si vuole eseguire una istruzione differente a seconda del valore di una espressione booleana. In questo caso si usa un costrutto particolare che ha la forma:


if (espressione booleana) 
	istruzione1;
else
	istruzione2;

Se l'istruzione è vera viene eseguita l'istruzione che segue, altrimenti viene eseguita l'istruzione che segue la parola chiave else.
L'esempio che abbiamo fatto prima può essere riscritto in modo più chiaro come:


int massimo(int a, int b) {
	int max;	

	if (a > b)
		max = a;
	else
		max = b;
		
	return max;
}			

In precedenza avevamo usato il trucco di inizializzare la variabile max a b, quindi controllare se a è maggiore di b, e nel caso il valore di max viene sostituito con a. Adesso possiamo lasciare non inizializzato max e effettuare il confronto. Se a è maggiore di b poniamo a in max, altrimenti vi poniamo b. La forma è più chiara e leggibile rispetto alla precedente. Lo scopo è subito visibile.

Una soluzione alternativa per questa operazione è quella di usare l'operatore ?, il quale genera un codice più conciso ma anche meno comprensibile.


int massimo(int a, int b) {
	int max;	

	max = (a > b) ? a : b;
		
	return max;
}			

Costrutto if.. else if

Quando si ha più di una opzione, si devono mettere più istruzioni if in cascata:


int giorni_del_mese(int mese) {
	giorni;

	if (mese == Gennaio)
		giorni = 31;
	if (mese == Febbraio)
		giorni = 28;
	
	...

	return giorni;
}	

Una forma alternativa, più potente è quella che fa uso del costrutto if..else if


int giorni_del_mese(int mese) {
	giorni;

	if (mese == Gennaio)
		giorni = 31;
	else if (mese == Febbraio)
		giorni = 28;
	
	...
	
	return giorni;
}	

Mentre nel primo caso tutti i confronti in cascata vengono comunque effettuati, nel secondo caso se la prima condizione è vera tutti gli altri else if vengono direttamente ignorati.

Istruzione switch..case

Lo stesso esempio precedente è il tipico caso in cui si può applicare il costrutto switch. La forma generale è:


switch (espressione) {
	case costante1:
	sequenza di istruzioni;
	break;

	case costante2:
	sequenza di istruzioni;
	break;

	case costante3:
	sequenza di istruzioni;
	break;

	...

	default:
	sequenza di istruzioni;
}		

Data un espressione, questa viene confrontata con l'elenco di costanti riportate con le parole chiave case. Il confronto avviene nell'ordine dall'alto verso il basso. Quando viene trovata la corrispondenza cercata vengono eseguite le istruzioni che seguono fino alla parola chiave break. La sezione default, opzionale, indica come comportarsi se non viene trovata alcuna corrispondeza.


int giorni_del_mese(int mese) {
	giorni;

	switch (mese) {
		case Gennaio:
		giorni = 31;
		break;

		case Febbraio:
		giorni = 28;
		break;

		...
	}	

	return giorni;	
}

Lo switch può effettuare solo verifiche di uguaglianza, ed è quindi meno generica rispetto alla forma if..else if, però permette forme molto potenti come la seguente:


int giorni_del_mese(int mese) {
	giorni;

	switch (mese) {
		case Febbraio:
		giorni = 28;
		break;

		case Aprile:
		giorni = 30;
		break;

		case Giugno:
		giorni = 30;
		break;

		case Settembre:
		giorni = 30;
		break;

		case Novembre:
		giorni = 30;
		break;

		default:
		giorni = 31;
	}
		
	return giorni;		
}

In questo caso si accomuna il caso più frequente come "default", che sono i mesi con 31 giorni e gli altri rappresentano le eccezioni: i mesi a 30 giorni e febbraio a 28. Una versione più complessa della funzione prevede anche il calcolo degli anni bisestili per i giorni di Febbraio:


int giorni_del_mese(int mese, int anno) {
	giorni;

	switch (mese) {
		case Febbraio:
		if ( bisestile(anno) ) 	
			giorni = 29;			
		else
			giorni = 28;
		break;

		case Aprile:
		giorni = 30;
		break;

		case Giugno:
		giorni = 30;
		break;

		case Settembre:
		giorni = 30;
		break;

		case Novembre:
		giorni = 30;
		break;

		default:
		giorni = 31;
	}
		
	return giorni;		
}

In cui i due costrutti switch..case e if..else sono combinati per lavorare insieme.

Un ultima nota: l'istruzione break è opzionale. Serve per concludere l'esecuzione della sequenza legata all'occorrenza trovata. Nel caso in cui mancasse l'istruzione break, l'esecuzione continuerebbe ignorando le altre occorrenze fino alla prima istruzione break o alla conclusione del costrutto.
Per chiarire si consideri l'esempio seguente:


int giorni_del_mese(int mese, int anno) {
	giorni;

	switch (mese) {
		case Febbraio:
		if ( bisestile(anno) ) 	
			giorni = 29;			
		else
			giorni = 28;
		break;

		case Aprile:
		case Giugno:
		case Settembre:
		case Novembre:
		giorni = 30;
		break;

		default:
		giorni = 31;
	}
		
	return giorni;		
}

Nei mesi di Aprile, Giugno e Settembre manca sia l'istruzione che il break. Questo vuol dire che se capitasse una qualunque di queste tre occorrenze l'istruzione eseguita è quella di Novembre. Questa opzione è utile nei casi in cui ci sono istruzioni identiche.

Istruzioni di controllo del flusso

Le istruzioni di controllo del flusso permettono di effettuare dei cicli condizionati da espressioni. Fanno parte di questa categoria il ciclo for, il ciclo while, il ciclo do..while e il comando goto.

Il ciclo for

Si tratta di uno dei costrutti più usati nella programmazione in genere, in quanto permette di effettuare delle iterazioni controllate. La forma generale è:


for (inizializzazione; condizione; incremento)
	istruzione;

L'inizializzazione serve ad impostare la variabile di controllo del ciclo. La condizione è un'espressione booleana che determina l'uscita dal ciclo. L'incremento definisce come varia la variabile di controllo. Ecco un esempio abbastanza tipico:


for (mese = 0; mese < 12; mese++) {
	giorni = giorni_del_mese(mese);
	printf("Giorni del mese %d:%d",mese,giorni);
}

La variabile di controllo è mese, e viene fatta variare da 0 (= GENNAIO) fino a 11 (= DICEMBRE) con incrementi unitari. Si noti che la condizione mese < 12 fa uscire dal ciclo quando non è verificata, cioè quando da valore falso. Nel blocco di istruzioni stiamo usando la funzione che abbiamo definito sopra per scrivere su schermo tutti i giorni dei mesi.

Analizziamo cosa accade. Quando si incontra la prima volta l'istruzione for, viene inizializzata la variabile di controllo (mese = 0) e viene effettuato il controllo della condizione (mese < 12); se questo da falso si esce dal ciclo, altrimenti si comincia dalla prima istruzione del blocco. Dopo aver eseguito tutte le istruzioni comprese tra le parentesi graffe viene incrementata la variabile di controllo secondo la regola descritta (mese++ equivalente a mese = mese + 1). Quindi si ricomincia dal controllo della condizione.

Quello visto non è l'unico modo di usare questo costrutto, ma è il più corretto dal punto di vista logico: si usa quando un operazione deve essere ripetuta per un numero determinato di volte. Vediamo qualche altra variante:


for (x=0, y=0; x+y<10; x++,y++)
	accendi_pixel(x,y,Blu);	

Come si vede le variabili di controllo sono due, grazie all'operatore virgola che ci permette di posizionare più espressioni nel posto in cui ce ne sta una. Mettendo più di una variabile di controllo il ciclo inizia ad essere meno determinato di quello di partenza; ad esempio:


for (mese=0, anno=1999; !(anno==2001 && mese==11), mese++) {
	if (mese==12) {
		mese=0;
		anno++;
	}
	giorni = giorni_del_mese(mese);
	printf("Giorni del mese %d:%d",mese,giorni);
}		

Qui le variabili di controllo sono mese ed anno. Solo mese viene incrementata. Quando mese supera 11 (che corrisponde a Dicembre) viene riportata a 0 (Gennaio) e viene incrementato l'anno. Per ogni mese degli anni compresi tra 1999 e 2001 (incluso) vengono stampati il numero di giorni.
Il codice non fa ciò che si ci aspetta. Infatti la condizione di uscita (che viene verificata prima di entrare nel ciclo) fa si che i giorni di Dicembre 2001 non vengano stampati.

Una soluzione più efficace è quella di porre due cicli for uno dentro l'altro:


for (anno=1999; anno<2002, anno++)
	for (mese = 0; mese < 12; mese++) {
		giorni = giorni_del_mese(mese);
		printf("Giorni del mese %d:%d",mese,giorni);
	}

Il primo ciclo controlla l'anno, il secondo controlla i mesi. Si noti che solo il ciclo più interno ha bisogno delle parentesi graffe, perché l'intero blocco:


for (mese = 0; mese < 12; mese++) {
	giorni = giorni_del_mese(mese);
	printf("Giorni del mese %d:%d",mese,giorni);
}

è un unica istruzione.

Cicli infiniti mediante costrutto for

Poiché nessuna delle tre istruzioni di ciclo è obbligatoria, è facile creare un ciclo infinito:


for ( ; ; )
	printf("Per sempre ");

Il ciclo while

Altra istruzione per generare dei cicli è la parola chiave while. La forma generale è:


while (condizione)
	istruzione;

La condizione è una espressione boolena. L'istruzione viene eseguita finché la condizione rimane vera. Non appena la condizione diviene falsa il controllo viene passato all'istruzione successiva. Ad esempio:


int count = 0;

while (count<100)
	count++;

printf("%d",count);

il ciclo continua finché la variabile count non è più minore di 100, ovvero non appena raggiunge il valore 100.

Il controllo viene eseguita prima di eseguire l'istruzione, per cui se vogliamo stampare i primi numeri interi minori di 10,


int count = 0;

while (count<10) {
	count++;
	printf("%d ",count);
}

dobbiamo stare attenti. Nell'esempio al posto della singola istruzione si mette un blocco, mediante l'uso delle parentesi graffe. L'output fornito da questa porzione di codice è:


1 2 3 4 5 6 7 8 9 10

C'è quindi un errore dovuto al fatto che non volevamo stampare il numero 10. Come è possibile? Per rispondere analizziamo cosa succede. Quando si entra nel ciclo la variabile count vale 0. Poichè questo soddisfa la condizione si esegue il blocco di istruzioni: si incrementa count e si stampa il contenuto. Quindi si ritorna al controllo della condizione. Fino a che la variabile contiene il valore 9 il blocco viene eseguito. La variabile viene incrementata ancora e arriva a 10 e il suo valore viene stampato. Al prossimo controllo count<10 restituisce falso e si esce dal ciclo.

L'errore è dovuto quindi ad aver posizionato l'istruzione di stampa tra l'incremento e il controllo. Una versione corretta è:


int count = 1;

while (count<10) {
	printf("%d ",count);
	count++;
}

che da un output corretto:


1 2 3 4 5 6 7 8 9

Questo perchè l'istruzione di stampa viene subito dopo il controllo, quindi il contenuto della variabile soddisfa la condizione di controllo.

La presenza di tanti costrutti per creare cicli ci rende felici, ma una domanda che nasce è "quando è meglio usare il cilco while rispetto al ciclo for?".
La risposta è che non esiste una regola. Con entrambi i costrutti si possono ottenere gli stessi risultati. Allora per rispondere entra in gioco il solito fattore di leggibilità del codice. Ognuno dei due costrutti può essere impiegato con maggiore chiarezza in diversi ambiti.
Abbiamo già detto che il ciclo for è un ciclo deterministico, in cui la variabile di controllo assume dei valori "controllati". Il ciclo while può essere impiegati in quei casi in cui non si conosce a priori il conenuto della variabile. Ad esempio quando questa contiene un carattere digitato da tastiera o un valore letto da un file.


char carattere = 0;

while ( (carattere = aspetta_carattere()) == SPAZIO) {
	...
}

Nell'esempio si possono notare due cose. L'uso dell'assegnamento all'interno di una espressione (carattere = aspetta_carattere()) e il concetto di cui parlavamo sopra. Nessuno può sapere quale tasto digiterà l'utente da tastiera, e finchè questo è uno SPAZIO il ciclo verrà ripetuto. Ad ogni controllo alla variabile verrà assegnato un nuovo carattere difitato e questo verrà confrontato con lo SPAZIO.

Cicli infiniti mediante costrutto while

Anche con il costrutto while è possibile generare dei cicli infiniti. Eccone la forma:


while (0<10) {
	...
}

Poichè la condizione è sempre vera il ciclo viene ripetuto all'infinito. Un altro metodo è quello di mettere una variabile con valore diverso da zero.


int test = 10

while (test) {
	...
}

Poichè in C una espressione positiva è considerata vera, mentre lo zero è considerato falso, il cilco viene ripetuto all'infinito.

Il ciclo do..while

A differenza dei cicli for e while, il ciclo do..while effettua il controllo della condizione alla fine dell'istruzione e non all'inizio. Quindi il codice viene eseguito almeno una volta. La forma genrale è:


do {
	istruzione1;
	istruzione2;
		...
} while (condizione);

Anche se le parentesi graffe non sono strettamente necessarie è buona norma usarle sempre anche quando si ha una sola istruzione per evitare confusione. Il ciclo continua finché la condizione non risulta falsa.

Anche in questo caso non esiste nessun caso in cui questo costrutto risulta indispensabile. Tuttavia è stato introdotto per questioni di comodità.


int raggio;
int	test;

pulisci_lo_schermo();

do {
	test = TRUE;

	printf("Inserisci raggio:");

	raggio = leggi_da_tastiera();

	if (raggio < 0) {
		test = FALSE;
		printf("Inserire un valore positivo");
	}

	if (raggio > 100) {	
		test = FALSE;
		printf("Numero che supera le capacità di calcolo della macchina");
	}				
		
} while (test == FALSE);

Bibliografia


Testi consigliati per l'apprendimento

Questo articolo è stato scaricato dal Club di informatica
Pagina curata da Luca Sabatucci